Skip to content

fix(studio): undo paints a move or nudge back at once while its save is still running - #4877

Merged
miguel-heygen merged 5 commits into
mainfrom
studio/undo-paints-pending-edit-revert
Oct 2, 2026
Merged

miguel-heygen merged 5 commits into
mainfrom
studio/undo-paints-pending-edit-revert

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

What

If you press Cmd+Z while a move or a nudge is still saving, the box now jumps back to where it was in the same frame as the key. Before, it stayed at the moved place until the save and the server's undo had both landed. Under load that was often a second or more, so a drag started from what was on screen then jumped.

Why

Undo's instant paint (from #4798) predicts the step from this tab's history and shows it in place. An edit that is still saving has no history entry yet, so the prediction would show the edit before it, and the paint is refused. The screen then waited for the server.

The same gap stayed open briefly after each save: the save ended before the history list holding its entry had been fetched, so for that moment the edit was neither pending nor known to the history.

How

  • A pending edit can now carry a live revert: a function that shows the edit undone and returns one that shows it again.
  • The canvas move (single and group) and the arrow-key nudge burst register their saves as pending edits with a revert. The revert puts the moved elements back at their offset from the start of the gesture. Elements GSAP positions have no revert, so they still wait as they do today.
  • When the instant paint is refused, undo paints the newest pending edit back, in the key's own task. Only the newest is painted: one Cmd+Z undoes one edit. A second Cmd+Z while that edit is still saving paints nothing and waits.
  • If the server step fails or steps nothing, the edit is shown again.
  • A nudge burst that undo painted back while it was still waiting for keys is still saved, so the server can undo it, but its commit no longer draws it again.
  • Anything an edit starts inside its own registration (the tracked context action, a GSAP commit) counts as part of that edit, not as a newer one. An edit whose start throws ends its registration, so it cannot hold up later undos and exports.
  • recordEdit now waits for the history list it fetches after a claim. A save therefore ends only once undo can predict it, which closes the gap above. The fetch was already requested after every claim (void refresh()); it is now awaited, so there is no extra request, and a save ends when that fetch returns instead of before it.
  • Bench: undo and a text step's Enter now wait a frame before the next step reads the box, as a nudge already does per key.
  • seqrepeat-none-px-r0-nested-z100 and sequndo-none-px-r0-root-z100 are out of the quarantine list. The quarantine list is now empty.

Why the bench needed the frame

The drag that follows an undo read the box with CDP DOM.getContentQuads 0 to 4 ms after Studio had already moved it back in the DOM. In the failing runs that read still returned the spot before the undo, while getBoundingClientRect on the same element right after returned the spot after it. Reads that passed had a frame between the undo and the read.

Traced build of this branch before the recordEdit wait (gsap=none, 6 cases a run) Read the spot before the undo Move, undo, drag back passes
Bench as on main, 4 runs 1 to 4 of 6 per run 2 to 5 of 6 per run
Bench waiting a frame after the key, 3 runs 0 of 18 17 of 18, the miss being the gap fixed by the recordEdit wait

So the bench pressed where the box had been before the frame that showed the undo. Two frames later, at mouse down, the box was no longer there, and the press started no drag. Before this PR the box stayed put until the server's undo, which hid the race.

Bench

On one machine, 3 jobs, three runs of each build, with this branch's bench:

Sequence (gsap=none) main this PR
move, undo, drag back (6 cases) 18/18 18/18
nudge one key, undo on the next frame (6 cases) 18/18 18/18
move, move, move, then the undo walk (px, nested) 3/3 3/3

Both builds pass every run once the bench waits for the frame, so this table shows no regression; it does not measure the gain. The gain is how soon the box shows undone, which is in the captures below. "This PR" is the build one commit before the head; the only later change removes a code comment. In CI the gate passes, with nothing newly passing or unbanked: main's baseline already banks both ids taken off the quarantine list as passing.

Test plan

  • studioPendingEdits.test.ts: only the newest edit is painted back, once, and shown again on request; nothing is painted when the newest edit has no revert; what an edit starts inside its registration is part of it; an edit whose start throws ends its registration. The last fails when a throwing start does not end the registration.

  • dragUndoPaint.test.ts: a drag whose save is still running can be painted back at once and shown again, and stops counting once its save lands. Fails when the drag registers no revert.

  • useEditHistoryActions.paint.test.tsx, over the real history engine:

    • an undo pressed while a nudge's save is queued shows the nudge undone in the key's own task, and the file and the box end undone;
    • an undo inside the nudge's wait shows the nudge undone at once, and the burst's commit doesn't draw it again.

    Both fail without the revert paint; the second also fails when the burst's commit redraws it.

  • usePersistentEditHistory.test.ts: with the history list slowed by 50 ms, the step is predicted as soon as the save ends. Fails when recordEdit does not wait for the list.

  • Bench unit tests (tests/e2e/edit-accuracy), the Studio suite, typecheck, oxlint, oxfmt.

Before

A move in a bench fixture with every save slowed to 1.5 s, Ctrl+Z on the frame after release, on #4807: the box stays at the moved place until the server's undo lands.

before-queued-undo

After

The same on this branch: the box is back in the first frame after the key and stays there.

How to read the panels: each one is the last frame the browser's screencast sent at or before that time. The screencast sent no frame between 107 ms before the key and 107 ms after it, so the "50 ms after" panel repeats the frame from before the key. The first frame after the key, at 107 ms, already shows the box back at its start; the selection outline has caught up by the next frame it sent, at 209 ms. The strip cannot show how soon the box paints. That comes from the tests, which check that the box is back in the key's own task. The capture was taken on a build of the first commit of this branch; later commits change neither the move nor its revert.

after-queued-undo

@miguel-heygen
miguel-heygen force-pushed the studio/undo-waits-for-committed-edit branch from b16fca2 to a3e9889 Compare October 1, 2026 18:50
@miguel-heygen
miguel-heygen force-pushed the studio/undo-paints-pending-edit-revert branch from 3c782fb to 4235a4e Compare October 1, 2026 18:52
@miguel-heygen
miguel-heygen force-pushed the studio/undo-waits-for-committed-edit branch from a3e9889 to bedf393 Compare October 1, 2026 19:23
@miguel-heygen
miguel-heygen force-pushed the studio/undo-paints-pending-edit-revert branch from 4235a4e to a966766 Compare October 1, 2026 19:56
@miguel-heygen
miguel-heygen force-pushed the studio/undo-waits-for-committed-edit branch from c527274 to 0532755 Compare October 1, 2026 21:22
Base automatically changed from studio/undo-waits-for-committed-edit to main October 1, 2026 22:22
@miguel-heygen
miguel-heygen force-pushed the studio/undo-paints-pending-edit-revert branch from a966766 to 017b325 Compare October 1, 2026 22:37
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Edit accuracy: 953 passing here, 953 on the base branch

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (2)

  • seqnudge-none-pct-r0-nested-z100: tracking 0.03, pressJump 0, drop 0, reload 0, render 0.01, undo false, teleport true / tracking 0.03, pressJump 0, drop 0, reload 0, render 0.01, undo true, teleport true / tracking 0.03, pressJump 0, drop 0, reload 0, render 0.01, undo true, teleport true
  • seqresize-none-px-r0-nested-z100: tracking 0.21, pressJump 0, drop 0, reload 0, render 0.03, undo false, teleport true / tracking 0.21, pressJump 0, drop 0, reload 0, render 0.03, undo true, teleport true / tracking 0.21, pressJump 0, drop 0, reload 0, render 0.03, undo true, teleport true

@miguel-heygen
miguel-heygen force-pushed the studio/undo-paints-pending-edit-revert branch 2 times, most recently from 98e2226 to 63e3aa2 Compare October 2, 2026 02:04
@miguel-heygen
miguel-heygen marked this pull request as ready for review October 2, 2026 02:50
@miguel-heygen
miguel-heygen force-pushed the studio/undo-paints-pending-edit-revert branch from 63e3aa2 to fcbb0b0 Compare October 2, 2026 02:58
@miguel-heygen
miguel-heygen force-pushed the studio/undo-paints-pending-edit-revert branch from fcbb0b0 to 2368918 Compare October 2, 2026 03:28
@miguel-heygen
miguel-heygen merged commit 880b4c4 into main Oct 2, 2026
111 of 114 checks passed
@miguel-heygen
miguel-heygen deleted the studio/undo-paints-pending-edit-revert branch October 2, 2026 04:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant